# -*- coding: utf-8 -*-
import re
import random
import json
import os
import sys
import datetime
import time
import threading
import logging
import pickle

from collections import deque
from HttpClient import HttpClient

reload(sys)
sys.setdefaultencoding("utf-8")

HttpClient_Ist = HttpClient()

ClientID = int(random.uniform(111111, 888888))
PTWebQQ = ''
APPID = 0
msgId = 0
FriendList = {}
GroupList = {}
ThreadList = []
GroupThreadList = []
GroupWatchList = []
PSessionID = ''
Referer = 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
SmartQQUrl = 'http://w.qq.com/login.html'
VFWebQQ = ''
AdminQQ = '0'

initTime = time.time()

logging.basicConfig(filename='Login.log', level=logging.DEBUG, format='%(asctime)s  %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S')

#
# @brief manager all for ai
# @note simpe ai, wait to to better
#
class aiManager():
    aiLock = threading.Lock()
    orgReply = False

    def __init__(self):
        self.restore()
    
    #You must lock the ai and first and then release the lock after use AI
    @staticmethod
    def lock_ai(self):
        self.aiLock.acquire()
    
    @staticmethod
    def release_ai(self):
        self.aiLock.release()

    def __init__(self):
        self.replyList = {}

    #direct learn words
    def direct_learn(self, key, value):
        key = key.decode('utf8')
        print 'learn key %s' % key

        if key in self.replyList:
            self.replyList[key].append(value)
        else:
            self.replyList[key] = [value]
        
    #direct delete words
    def direct_delete(self, key, value):
        if key in self.replyList and self.replyList[key].count(value):
            self.replyList[key].remove(value)
    
    #direct reply words
    def direct_reply(self, key):
        #print key
        if key and self.replyList.get(key, False):
            rd = random.randint(0, len(self.replyList[key]) - 1)
            return str(self.replyList[key][rd])
        else:
            print "key {%s} find ans fail" % key
            return False
    
    #debug the reply list
    def direct_debug(self):
        for key in self.replyList:
            print "key:%s" % key
            print self.replyList[key]
    
    #debug simple restore
    def direct_restore(self):
        file_object = open('ai.conf', 'rb') 
        try:   
            self.replyList = pickle.load(file_object)
        finally:   
            file_object.close()

    #debug simple dump
    def direct_dump(self):
        file_object = open('ai.conf', 'wb') 
        try:   
            pickle.dump(self.replyList, file_object)
        finally:   
            file_object.close()

#Local ai handle
ai = aiManager()

def pass_time():
    global initTime
    rs = (time.time() - initTime)
    initTime = time.time()
    return str(round(rs, 3))

def getReValue(html, rex, er, ex):
    v = re.search(rex, html)
    if v is None:
        logging.error(er)
        if ex:
            raise Exception, er
        else:
            print er
        return ''
    return v.group(1)

def date_to_millis(d):
    return int(time.mktime(d.timetuple())) * 1000

# 查询QQ号，通常首次用时0.2s，以后基本不耗时
def uin_to_account(tuin):
    # 如果消息的发送者的真实QQ号码不在FriendList中,则自动去取得真实的QQ号码并保存到缓存中
    global FriendList
    if tuin not in FriendList:
        try:
            info = json.loads(HttpClient_Ist.Get('http://s.web2.qq.com/api/get_friend_uin2?tuin={0}&type=1&vfwebqq={1}'.format(tuin, VFWebQQ), Referer))
            logging.info("Get uin to account info:" + str(info))
            if info['retcode'] != 0:
                raise ValueError, info
            info = info['result']
            FriendList[tuin] = info['account']
        except Exception as e:
            logging.error(e)
    logging.info("Now FriendList:" + str(FriendList))
    return FriendList[tuin]

#
# @process admin command
#
def command_handler(inputText):
    global GroupWatchList
    
    ##TODO too many branch for command switch could be better
    #command for group watch
    pattern = re.compile(r'^!(group|ungroup) (\d+)$')
    match = pattern.match(inputText)

    if match and match.group(1) == 'group':
        GroupWatchList.append(str(match.group(2)))
        print "group list:", GroupWatchList
    elif match and match.group(1) == 'ungroup':
        GroupWatchList.remove(str(match.group(2)))
        print "group list:", GroupWatchList
    else:
        pass

    #command for learn
    pattern = re.compile(r'^!(learn|delete)\s+{(.+)}\s+{(.+)}')
    match = pattern.match(inputText)

    if match and match.group(1) == 'learn':
        ai.direct_learn(match.group(2), match.group(3))
    elif match and match.group(1) == 'delete':
        ai.direct_delete(match.group(2), match.group(3))
    else:
        pass
    
    #command for debug ai
    pattern = re.compile(r'^!debug')
    match = pattern.match(inputText)
    if match:
        ai.direct_debug()

    #command for restore config
    pattern = re.compile(r'^!restore')
    match = pattern.match(inputText)
    if match:
        ai.direct_restore()

    #command for dump config
    pattern = re.compile(r'^!dump')
    match = pattern.match(inputText)
    if match:
        ai.direct_dump()

    return
#
#process user msg
#
def msg_handler(msgObj):
    for msg in msgObj:
        msgType = msg['poll_type']
        
        #TODO too may branch in this function
        # QQ私聊消息
        if msgType == 'message' or msgType == 'sess_message':  # 私聊 or 临时对话
            txt = combine_msg(msg['value']['content'])
            tuin = msg['value']['from_uin']
            msgid = msg['value']['msg_id2']
            from_account = uin_to_account(tuin)
            isSess = 0
            group_sig = ''
            service_type = ''
            # print "{0}:{1}".format(from_account, txt)
            targetThread = thread_exist(from_account)
            if targetThread:
                targetThread.tucao(txt, msgid)
            else:
                if msgType == 'sess_message':
                    isSess = 1
                    service_type = msg['value']['service_type']
                    myid = msg['value']['id']
                    ts = time.time()
                    while ts < 1000000000000:
                        ts = ts * 10
                    ts = int(ts)
                    info = json.loads(HttpClient_Ist.Get('http://d.web2.qq.com/channel/get_c2cmsg_sig2?id={0}&to_uin={1}&clientid={2}&psessionid={3}&service_type={4}&t={5}'.format(myid, tuin, ClientID, PSessionID, service_type, ts), Referer))
                    logging.info("Getting group sig :" + str(info))
                    if info['retcode'] != 0:
                        raise ValueError, info
                    info = info['result']
                    group_sig = info['value']
                    logging.info("Group sig: " + str(group_sig))
                tmpThread = pmchat_thread(tuin, service_type, group_sig, isSess)
                tmpThread.start()
                ThreadList.append(tmpThread)
                tmpThread.tucao(txt, msgid)

        # 群消息
        if msgType == 'group_message':
            #print "group message recive"
            global GroupList, GroupWatchList
            txt = combine_msg(msg['value']['content'])
            guin = msg['value']['from_uin']
            gid = msg['value']['info_seq']
            tuin = msg['value']['send_uin']
            seq = msg['value']['seq']
            GroupList[guin] = gid
            if str(gid) in GroupWatchList:
                g_exist = group_thread_exist(gid)
                if g_exist:
                    #print "群(%s)的消息: %s" %(str(gid), txt)
                    g_exist.handle(tuin, txt, seq)
                else:
                    tmpThread = group_thread(guin)
                    tmpThread.start()
                    GroupThreadList.append(tmpThread)
                    tmpThread.handle(tuin, txt, seq)
                    print "群线程已生成"
            else:
                    #print str(gid) + "群有动态，但是没有被监控"
                    pass

        # QQ号在另一个地方登陆, 被挤下线
        if msgType == 'kick_message':
            logging.error(msg['value']['reason'])
            raise Exception, msg['value']['reason']  # 抛出异常, 重新启动WebQQ, 需重新扫描QRCode来完成登陆

def combine_msg(content):
    msgTXT = ""
    for part in content:
        # print type(part)
        if type(part) == type(u'\u0000'):
            msgTXT += part
        elif len(part) > 1:
            # 如果是图片
            if str(part[0]) == "offpic" or str(part[0]) == "cface":
                msgTXT += "[图片]"
    return msgTXT

def send_msg(tuin, content, service_type, group_sig, isSess, failTimes=0):
    lastFailTimes = failTimes
    try:
        if isSess:
            reqURL = "http://d.web2.qq.com/channel/send_sess_msg2"
            data = (
                ('r', '{{"to":{0}, "face":594, "content":"[\\"{4}\\", [\\"font\\", {{\\"name\\":\\"Arial\\", \\"size\\":\\"10\\", \\"style\\":[0, 0, 0], \\"color\\":\\"000000\\"}}]]", "clientid":"{1}", "msg_id":{2}, "psessionid":"{3}", "group_sig":"{5}", "service_type":{6}}}'.format(tuin, ClientID, msgId, PSessionID, str(content), group_sig, service_type)),
                ('clientid', ClientID),
                ('psessionid', PSessionID),
                ('group_sig', group_sig),
                ('service_type', service_type)
            )

        else:
            reqURL = "http://d.web2.qq.com/channel/send_buddy_msg2"
            data = (
                ('r', '{{"to":{0}, "face":594, "content":"[\\"{4}\\", [\\"font\\", {{\\"name\\":\\"Arial\\", \\"size\\":\\"10\\", \\"style\\":[0, 0, 0], \\"color\\":\\"000000\\"}}]]", "clientid":"{1}", "msg_id":{2}, "psessionid":"{3}"}}'.format(tuin, ClientID, msgId, PSessionID, str(content))),
                ('clientid', ClientID),
                ('psessionid', PSessionID)
            )

        rsp = HttpClient_Ist.Post(reqURL, data, Referer)
        rspp = json.loads(rsp)
        if rspp['retcode'] != 0:
            logging.error("reply pmchat error"+str(rspp['retcode']))
        return rspp
    except:
        if lastFailTimes < 5:
            logging.error("Response Error.Wait for 2s and Retrying."+str(lastFailTimes))
            logging.info(rsp)
            lastFailTimes += 1
            time.sleep(2)
            send_msg(tuin, content, service_type, group_sig, isSess, lastFailTimes)
        else:
            logging.error("Response Error over 5 times.Exit.")
            raise ValueError, rsp

def thread_exist(tqq):
    for t in ThreadList:
        if t.tqq == tqq:
            return t
    return False

def group_thread_exist(gid):
    for t in GroupThreadList:
        if str(t.gid) == str(gid):
            return t
    return False

# 
# @brief Login function 
# 
class Login(HttpClient):
    MaxTryTime = 5

    def __init__(self, vpath, qq=0):
        global APPID, AdminQQ, PTWebQQ, VFWebQQ, PSessionID, msgId
        self.VPath = vpath  # QRCode保存路径
        AdminQQ = int(qq)
        print "正在获取登陆页面"
        self.initUrl = getReValue(self.Get(SmartQQUrl), r'\.src = "(.+?)"', 'Get Login Url Error.', 1)
        html = self.Get(self.initUrl + '0')

        print "正在获取appid"
        APPID = getReValue(html, r'var g_appid =encodeURIComponent\("(\d+)"\);', 'Get AppId Error', 1)
        print "正在获取login_sig"
        sign = getReValue(html, r'var g_login_sig=encodeURIComponent\("(.+?)"\);', 'Get Login Sign Error', 0)
        logging.info('get sign : %s', sign)
        print "正在获取pt_version"
        JsVer = getReValue(html, r'var g_pt_version=encodeURIComponent\("(\d+)"\);', 'Get g_pt_version Error', 1)
        logging.info('get g_pt_version : %s', JsVer)
        print "正在获取mibao_css"
        MiBaoCss = getReValue(html, r'var g_mibao_css=encodeURIComponent\("(.+?)"\);', 'Get g_mibao_css Error', 1)
        logging.info('get g_mibao_css : %s', sign)
        StarTime = date_to_millis(datetime.datetime.utcnow())

        T = 0
        while True:
            T = T + 1
            self.Download('https://ssl.ptlogin2.qq.com/ptqrshow?appid={0}&e=0&l=L&s=8&d=72&v=4'.format(APPID), self.VPath)
            print "登陆二维码下载成功，请扫描"
            logging.info('[{0}] Get QRCode Picture Success.'.format(T))
            print "下载二维码用时" + pass_time() + "秒"

            while True:
                html = self.Get('https://ssl.ptlogin2.qq.com/ptqrlogin?webqq_type=10&remember_uin=1&login2qq=1&aid={0}&u1=http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&ptredirect=0&ptlang=2052&daid=164&from_ui=1&pttype=1&dumy=&fp=loginerroralert&action=0-0-{1}&mibao_css={2}&t=undefined&g=1&js_type=0&js_ver={3}&login_sig={4}'.format(APPID, date_to_millis(datetime.datetime.utcnow()) - StarTime, MiBaoCss, JsVer, sign), self.initUrl)
                # logging.info(html)
                ret = html.split("'")
                if ret[1] == '65' or ret[1] == '0':  # 65: QRCode 失效, 0: 验证成功, 66: 未失效, 67: 验证中
                    break
                time.sleep(2)
            if ret[1] == '0' or T > self.MaxTryTime:
                break

        logging.info(ret)
        if ret[1] != '0':
            return
        print "二维码已扫描，正在登陆"
        pass_time()
        # 删除QRCode文件
        if os.path.exists(self.VPath):
            os.remove(self.VPath)

        # 记录登陆账号的昵称
        tmpUserName = ret[11]

        html = self.Get(ret[5])
        url = getReValue(html, r' src="(.+?)"', 'Get mibao_res Url Error.', 0)
        if url != '':
            html = self.Get(url.replace('&amp;', '&'))
            url = getReValue(html, r'location\.href="(.+?)"', 'Get Redirect Url Error', 1)
            html = self.Get(url)

        PTWebQQ = self.getCookie('ptwebqq')

        logging.info('PTWebQQ: {0}'.format(PTWebQQ))

        LoginError = 1
        while LoginError > 0:
            try:
                html = self.Post('http://d.web2.qq.com/channel/login2', {
                    'r': '{{"ptwebqq":"{0}","clientid":{1},"psessionid":"{2}","status":"online"}}'.format(PTWebQQ, ClientID, PSessionID)
                }, Referer)
                ret = json.loads(html)
                LoginError = 0
            except:
                LoginError += 1
                print "登录失败，正在重试"

        if ret['retcode'] != 0:
            return

        VFWebQQ = ret['result']['vfwebqq']
        PSessionID = ret['result']['psessionid']

        print "QQ号：{0} 登陆成功, 用户名：{1}".format(ret['result']['uin'], tmpUserName)
        logging.info('Login success')
        print "登陆二维码用时" + pass_time() + "秒"
        msgId = int(random.uniform(20000, 50000))

#
# @brief core thread, as a core pull msg from web
#
class check_msg(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        global PTWebQQ

        E = 0

        # 心跳包轮询
        while 1:
            if E > 5:
                break
            try:
                ret = self.check()
            except:
                E += 1
                continue
            # logging.info(ret)

            # 返回数据有误
            if ret == "":
                E += 1
                continue

            # POST数据有误
            if ret['retcode'] == 100006:
                break

            # 无消息
            if ret['retcode'] == 102:
                E = 0
                continue

            # 更新PTWebQQ值
            if ret['retcode'] == 116:
                PTWebQQ = ret['p']
                E = 0
                continue

            # 收到消息
            if ret['retcode'] == 0:
                # 信息分发
                msg_handler(ret['result'])
                E = 0
                continue

        print "轮询错误超过五次"

    # 向服务器查询新消息
    def check(self):

        html = HttpClient_Ist.Post('http://d.web2.qq.com/channel/poll2', {
            'r': '{{"ptwebqq":"{1}","clientid":{2},"psessionid":"{0}","key":""}}'.format(PSessionID, PTWebQQ, ClientID)
        }, Referer)
        logging.info("Check html: " + str(html))
        try:
            ret = json.loads(html)
        except Exception as e:
            logging.error(e)
            print "Check error occured, retrying."
            return self.check()

        return ret

#
# @private chat 
#
class pmchat_thread(threading.Thread):

    inputs = []
    # con = threading.Condition()
    stage = 0
    # newIp = ''

    #
    # @brief init pmchat thread
    #
    def __init__(self, tuin, service_type, group_sig, isSess):
        threading.Thread.__init__(self)
        self.tuin = tuin
        self.tqq = uin_to_account(tuin)
        self.inputs = []
        self.isSess = isSess
        self.service_type = service_type
        self.group_sig = group_sig
        self.lastMsgId = 0
        self.queue = deque(["打酱油"])
    
    #
    # @brief send msg to other
    #
    def reply(self, content):
        send_msg(self.tuin, str(content), self.service_type, self.group_sig, self.isSess)
        logging.info("Reply to " + str(self.tqq) + ":" + str(content))

    #
    # @brief get reply by msg
    #
    def tucao(self, ipContent, msgid):
        if msgid != self.lastMsgId:
            ans = ai.direct_reply(ipContent.strip())
            if ans:
                    self.queue.append(ans)
        else:
            logging.info("pm message repeat detected.")
        self.lastMsgId = msgid
    
    #
    # @brief pop msg and reply to web 
    #
    def run(self):
        while True:
            time.sleep(2)
            if len(self.queue):
                ans = self.queue.popleft()
                self.reply(ans)

#
# @brief group manager
#
class group_thread(threading.Thread):
    last1 = ''
    lastseq = 0

    # attribute
    repeatPicture = False

    #
    # @brief init group thread
    #
    def __init__(self, guin):
        threading.Thread.__init__(self)
        self.guin = guin
        self.gid = GroupList[guin]
        self.queue = deque(["打酱油"])

    #
    # @brief send msg to web
    #
    def reply(self, content):
        reqURL = "http://d.web2.qq.com/channel/send_qun_msg2"
        data = (
            ('r', '{{"group_uin":{0}, "face":564,"content":"[\\"{4}\\",[\\"font\\",{{\\"name\\":\\"Arial\\",\\"size\\":\\"10\\",\\"style\\":[0,0,0],\\"color\\":\\"000000\\"}}]]","clientid":"{1}","msg_id":{2},"psessionid":"{3}"}}'.format(self.guin, ClientID, msgId, PSessionID, content.replace("\\", "\\\\\\\\"))),
            ('clientid', ClientID),
            ('psessionid', PSessionID)
        )
        logging.info("Reply package: " + str(data))
        rsp = HttpClient_Ist.Post(reqURL, data, Referer)
        if rsp:
            print "[reply content]:", content, "[rsp]:", rsp
            logging.info("[Reply to group " + str(self.gid) + "]:" + str(content))
        return rsp

    #
    # @brief process msg from other
    #
    def handle(self, send_uin, content, seq):
        if seq != self.lastseq:
            self.tucao(content.strip())
            return
        else:
            print "message seq repeat detected."

        self.lastseq = seq
    
    #
    # @brief get reply by content from ai
    #
    def tucao(self, content):
        #print "tucao for content %s" % content 
        ans = ai.direct_reply(content)
        if ans:
            print "push %s into group output queue" % (ans)
            self.queue.append(ans)

    #
    # @brief run, pop reply from queue and send it to web
    #
    def run(self):
        while True:
            time.sleep(2)
            if len(self.queue):
                ans = self.queue.popleft()
                self.reply(ans)
            
#
# @brief main 
# 
if __name__ == "__main__":
    vpath = './v.jpg'
    qq = 0
    if len(sys.argv) > 1:
        vpath = sys.argv[1]
    if len(sys.argv) > 2:
        qq = sys.argv[2]

    try:
        pass_time()
        qqLogin = Login(vpath, qq)
    except Exception, e:
        print e

    t_check = check_msg()
    t_check.setDaemon(True)
    t_check.start()

    print '机器人开始工作'

    while 1:
        if not t_check.isAlive():
             exit(0)

        console_input = raw_input(">>")
        try:
            command_handler(console_input)
        except e:
            print "指令有误重新来", e
